package typejump.objects;

import java.awt.image.BufferedImage;
import java.lang.reflect.Method;

import typejump.Settings;
import typejump.engine.BoundsInfo;
import typejump.engine.CollisionProps;
import typejump.engine.Drawable;
import typejump.engine.GameEngine;
import typejump.engine.RBounds;
import typejump.util.IntKeeper;

/**
 * Abstract class which all GameObjects inherit. Represents a thing
 * that has bounds that can be drawn on a screen and might have physics
 * and collision behavior and stuff.
 * @author g
 *
 */
public abstract class GameObject extends RBounds implements Drawable {
	protected RBounds last     = new RBounds();
	
	protected volatile GameObject groundedOn = null;
	protected double gravity   = Settings.getDouble("default.gravity");
	protected double gravx     = 0; // horizontal gravity, not used at the moment
	
	protected boolean dynamic  = false; // True if this object moves around (rather than being static and fixed)
	protected boolean canFall  = false; // True if this is affected by gravity
	protected boolean physical = false; // True if other things can hit this
	protected boolean moveable = false; // True if other objects can shove this around
	protected boolean doremove = false; // True if this object should be removed
	
	private CollisionProps props = null;
	
	private IntKeeper info = new IntKeeper();
	
	private double curgravy = gravity;
	
	private String id = "";
	
	public GameObject() {
		ObjectManager.add(this);
	}
	
	@Override
	public GameObject clone() {
		return duplicate(generateSpawnCommand());
	}
	
	public GameObject duplicate(String params) {
		Method method = null;
		try {
			method = getClass().getMethod("createObject", String.class);
			GameObject o = (GameObject) method.invoke(null, params);
			o.centerX(centerX());
			o.centerY(centerY());
			return o;
		} catch (Exception e) {

		}
		return null;
	}
	
	public String getId() {
		return id;
	}
	
	public void setId(String id) {
		if (this.id.equals(id))
			return;
		this.id = id;
		GameEngine.registerId(id, this);
	}
	
	public boolean usesPasses() {
		return false;
	}
	
	/**
	 * Generates the spawn command that can be used to
	 * create a new object like this one.
	 * @return
	 */
	public String generateSpawnCommand() {
		return "";
	}
	
	public BufferedImage generateThumbnail(int w, int h) {		
		BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
		BufferedImage can = new BufferedImage(iw(), ih(), BufferedImage.TYPE_INT_ARGB);
		
		double byWidth = w/width(); // n*width()=width, n=width/width()
		double byHeight = h/height();

		double scale = Math.min(byWidth, byHeight);
		
		int nw = (int)(scale * width());
		int nh = (int)(scale * height());
		
		java.awt.Graphics2D g = can.createGraphics();
		paint(g);
		g.dispose();
		g = img.createGraphics();
		g.drawImage(can, 0, 0, nw, nh, null);
		g.dispose();
		return img;
	}
	
	/** Returns true if this object can collide with other object (not the inverse) */
	public boolean canHit(GameObject other) {
		if (props == null || other.props == null)
			return false;
		return props.canHit(other.props);
	}
	
	/**
	 * Defines the collision behavior of this object
	 * @param myType - What kind of things I am
	 * @param hitTypes - What kind of things I can hit.
	 */
	protected void setCollision(int myType, int hitTypes) {
		setCollision(new CollisionProps(myType, hitTypes));
	}
	
	/** Sets the collision behavior of this object */
	protected void setCollision(CollisionProps props) {
		this.props = props;
	}
	
	/** Returns true if this object isn't about to be deleted */
	public boolean enabled() {
		return !doremove;
	}
	
	/**
	 * Returns true if this object has been flagged for deletion.
	 * @return
	 */
	public boolean shouldRemove() {
		return doremove;
	}
	
	/**
	 * Sets a flag on this object so that it will be removed by the next pass of the engine.
	 */
	public void remove() {
		this.doremove = true;
	}
	
	/**
	 * Sets the vertical acceleration this object will experience when it isn't grounded
	 * @param y
	 */
	protected void setBaseGravity(double y) {
		curgravy = gravity = y;
	}
	
	/**
	 * Returns true if this object contains the given point
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean contains(double x, double y) {
		if (x < left() || x > right()) return false;
		if (y < top()  || y > bottom()) return false;
		return true;
	}
	
	/**
	 * Returns the object that this game object is sitting on (or null)
	 * @return
	 */
	public GameObject getStandingOn() {
		return groundedOn;
	}
	
	/**
	 * Returns the BoundsInfo representing the last position
	 * of this game object.
	 * @return
	 */
	public BoundsInfo last() {
		return last;
	}
	
	/**
	 * Sets the acceleration this object will experience in the
	 * absence of a floor
	 * @param x
	 * @param y
	 */
	public void setGravity(double y) {
		this.curgravy = y;
	}
	
	/**
	 * Reverts to this object's base gravity
	 */
	public void revertGravity() {
		this.curgravy = gravity;
	}
	
	/** Extra variables that can be used for whatever */
	public IntKeeper vars() {
		return info;
	}

	/** Returns how fast this accelerates downwards (pixels/second) when it's not on the ground. */
	public double getGravity() {
		return curgravy;
	}
	
	/** Returns true if this object is resting on a surface (not falling) */
	public boolean isOnGround() {
		return groundedOn != null;
	}
	
	/** True if this object moves around (rather than being static and fixed) */
	public boolean isDynamic() {
		return dynamic;
	}
	
	/** True if this is affected by gravity */
	public boolean canFall() {
		return canFall;
	}
	
	/** True if other things can hit this */
	public boolean isPhysical() {
		return physical;
	}
	
	/** True if other objects can shove this around */
	public boolean isMoveable() {
		return moveable;
	}
	
	/**
	 * Returns true if the bounds of this object intersect
	 * that of another
	 * @param other
	 * @return
	 */
	public boolean intersects(GameObject other) {
		return rect.intersects(other.rect);
	}
	
	/**
	 * Performs any special behavior that might occur when two objects collide
	 * (eg, deletes itself and creates an explosion)
	 * @param other
	 */
	public void collide(GameObject other) {
		return;
	}
	
	/**
	 * Performs a collision, like the normal collide() method.
	 * This is used for some of the fancy stuff in Surface to make
	 * sure bad things don't happen (like glitchy wall-jumping). Most
	 * subclasses can safely ignore it.
	 * @param other
	 * @param pass
	 */
	public void collide(GameObject other, int pass) {
		if (pass == 0) collide(other);
	}
	
	/**
	 * Returns true if it was on the ground and it isn't anymore
	 * @return
	 */
	public boolean checkGrounding() {
		if (canFall() && isOnGround()) {
			if (bottom() < groundedOn.top() - 1
					|| left() >= groundedOn.right() 
					|| right() <= groundedOn.left()) {
				groundedOn = null;
				return true;
			} else if (vy() > groundedOn.vy()) {
				vy(groundedOn.vy());
			}
		}
		return false;
	}
	
	/**
	 * Updates the position of this entity, including
	 * checking whether it's on the ground and applying
	 * gravity appropriately, and incrementing the
	 * x, y coordinates according to the current velocity.
	 * @param seconds
	 */
	public void updatePosition(double seconds) {
		last.set(this);

		checkGrounding();
		if (canFall()) {
			if (!isOnGround()) {
				vy += getGravity() * seconds;
			} 
		}
		
		moveBy(vx * seconds, vy * seconds);
	}
	
	/**
	 * Translates this object by the given x, y coordinates.
	 * @param x
	 * @param y
	 */
	public void moveBy(double x, double y) {
		rect.x += x;
		rect.y += y;
	}
	
	/**
	 * Increments the x and y velocity vectors
	 * by the given quantities (can be negative).
	 * @param x
	 * @param y
	 */
	public void accelerate(double x, double y) {
		this.vx += x;
		this.vy += y;
	}
	
	/**
	 * Decreases the magnitude of the x, y velocity
	 * vectors by the absolute values of the given 
	 * parameters. Example behavior:<br><br>
	 * 1. vel = <5, -7><br>
	 * 2. decelerate(8, 2)<br>
	 * 3. vel = <0, -5>
	 * @param x
	 * @param y
	 */
	public void decelerate(double x, double y) {
		x = Math.abs(x);
		y = Math.abs(y);
		
		if (vx > 0) {
			vx = Math.max(0, this.vx - x);
		} else {
			vx = Math.min(0, this.vy + x);
		}
		
		if (vy > 0) {
			vy = Math.max(0, this.vy - y);
		} else {
			vy = Math.min(0, this.vy + y);
		}
	}
	@Override
	public int getTranslateX() {
		return ix();
	}
	@Override
	public int getTranslateY() {
		return iy();
	}

	/**
	 * Updates this object's position, graphics, and any additional 
	 * implementation-specific things.
	 * @param time
	 */
	public abstract void update(double time);
}
